-----------Crusade in Europe-----------
A 4am and san inc crack      2017-08-06
---------------------------------------

Name: Crusade in Europe
Version: 1 (single-sided, 64K only)
Genre: simulation
Year: 1985
Credits: by Sid Meier and Ed Bever,
  Apple II version by Jim Synoski
Publisher: Microprose Software
Platform: Apple ][+ or later (64K)
Media: single-sided 5.25-inch floppy
OS: Diversi-DOS C1983
Previous cracks: none (of this version)

                   ~

               Chapter 0
    In Which We're Off And Running


The disk is a standard 16-sector disk,
except track $22 which is unreadable.
Searching for "BD 8C C0" leads us to
the track $0D sector $00, from the file
named "\\". (There's another copy of
this code on track $1E sector $08, but
it's unused.)

                 --v--

0273 BD 8C C0   LDA $C08C,X
0276 BD 8E C0   LDA $C08E,X
0279 20 44 F9   JSR $F944
027C B0 10      BCS $028E
027E AD 2E 02   LDA $022E
0281 4A         LSR
0282 C5 2E      CMP $2E
0284 D0 16      BNE $029C
0286 A9 DB      LDA #$DB
0288 8D 01 02   STA $0201
028B 4C 93 02   JMP $0293
028E A9 FF      LDA #$FF
0290 8D 01 02   STA $0201
0293 BD 88 C0   LDA $C088,X
0296 AD 81 C0   LDA $C081
0299 4C A4 02   JMP $02A4
029C A9 DB      LDA #$C0
029E 8D 01 02   STA $0201
02A1 4C 93 02   JMP $0293

                 --^--

We've found the protection routine!
Looks like a soft target. Let's just
patch $029D to #$DB and we're done,
right?

Wrong. While the game starts nicely, it
asks for a word from the manual before
starting any scenario. That's annoying,
and it's the second protection routine.

Let's enter the proper password and see
what happens. This is where everyone
until now made a critical mistake.
The game plays for a looong time and
everything looks fine. That is, until
it prints "Fatal error: nnn" (the
number changes each time) and hangs.

Okay, that leads to two possibilities:

  1. there's another protection check
     like the first one, or

  2. the protection check has a
     protection check of its own,
     i.e. an anti-tamper check

Actually, there's a third possibility:

  3. that both of those things are true

Let's find out.

                   ~

               Chapter 1
   In Which Our Fears Are Confirmed


The problem is that the program is
written in compiled Integer Basic, and
the result is interpreted at run-time
using a custom interpreter.

The p-code language is very simple,
composed primarily of comparisons,
transfers of control, and a couple of
arithmetic instructions and read/write
primitives.

The rest is I/O-related: fetching
keyboard input, setting various display
modes, cursor positioning, and
character printing.

It's faster than the original Basic,
and far more compact than native code,
but fast enough for the purpose.

It looks like this (and I have no idea
of the true names for the routines, I'm
just describing the behaviour):

                 --v--

.BYTE $12, $27, $00  ; jsr rel imm16
.BYTE $04, $AF, $61  ; push16 (imm16)
.BYTE $F6, $34       ; push16 imm8
.BYTE $24            ; add16 stk, stk
.BYTE $F6, $FF       ; push16 imm8
.BYTE $5C            ; push00xx (stk16)
.BYTE $C8            ; pop8 (stk16)

                 --^--

There's a dispatcher at $00AF (in zero
page), which does a load/store/jump.

Prior to that are several routines for
adjusting the instruction pointer by
popping from the stack, incrementing by
one, or adjusting according to a passed
parameter.

If we replace the store/jump with an
unconditional jump to spare memory, we
can watch the dispatcher in action. In
particular, we can see when it starts
to print the "FATAL ERROR" message, and
see who requested it. Once we find
that point, we can backtrack until we
find the start of that routine.

If we don't find the comparison that
triggers it, then we patch our
redirector to watch for someone about
to write the routine address in the
dispatcher, then use that address and
backtrack.

Lather, rinse, repeat, until we find
the comparison that sets off the whole
chain.

Time passes...

Sure enough, there's a comparison of
two 16-bit values, but not of the kind
that we expect.

Track $1F sector $0C, from the file
named "B":

                 --v--

.BYTE $04, $66, $53  ; push16 (imm16)
.BYTE $04, $84, $53  ; push16 (imm16)
.BYTE $2E            ; cmpne16 stk, stk
.BYTE $0E, $09, $00  ; btrue rel imm16
.BYTE $12, $FF, $11  ; jsr rel imm16

                 --^--

When the two values match, the string
is printed. That looks like a timer.

Now to find where those two values are
set.

More time passes...

$5384 is incremented monotonically and
reset periodically. It looks like a
frame counter.

$5366 is much more interesting. Here,
on track $1F sector $04, also from the
file named "B":

.BYTE $04, $66, $53  ; push16 (imm16)
.BYTE $F0            ; push0
.BYTE $32            ; cmplt16 stk, stk
.BYTE $10, $04, $00  ; btrue rel imm16
.BYTE $B6            ; rts
.BYTE $F0            ; push0
.BYTE $F6, $14       ; push16 imm8
.BYTE $AC, $68, $53  ; for loop
.BYTE $04, $68, $53  ; push16 (imm16)
.BYTE $06, $B6, $52  ; push16 (imm16+
                     ;   pop16*2)
.BYTE $F0            ; push0
.BYTE $2E            ; cmpne16 stk, stk
.BYTE $0E, $0C, $00  ; btrue rel imm16
.BYTE $F6, $48       ; push16 imm8
.BYTE $F6, $48       ; push16 imm8
.BYTE $5E            ; rand stk, stk
.BYTE $24            ; add16 stk, stk
.BYTE $08, $66, $53  ; pop16 (imm16)
.BYTE $B2, $68, $53  ; next
.BYTE $B6            ; rts

It turns out that $5366 is a flag to
indicate that a protection check
failed. It begins as $FFFF and is
checked periodically. The cmplt16
checks if $5366 is a negative
number. If so, then an array is parsed
via cmpne16 to possibly find a zero. If
one is found, then $5366 is replaced
with a RND(72)+72.

There's our timer, and we can find a
zero in that array.

But that array is scary. It means that
there's a whole set of protection
checks that might fail.

At least we know where to look. If we
instruct our dispatcher redirection to
watch for writes to those addresses,
we'll know when protection checks fail
and can backtrack to the check itself.

So, backtracking from there, and yes -
the initial protection routine has a
protection routine of its own, and yes,
it's a checksum. We've found the third
protection routine, and confirmed
possibility #2 -- the protection check
is itself protected by an anti-tamper
check.

                   ~

               Chapter 2
 In Which The Whole Is More (Or Less)
       Than The Sum Of Its Parts


The checksum code is on track $0D
sector $0B from the file named "A".
It looks like this:

                 --v--

.BYTE $AC, $E2, $52  ; for loop
.BYTE $04, $83, $54  ; push16 (imm16)
.BYTE $04, $E2, $52  ; push16 (imm16)
.BYTE $5C            ; push00xx (stk16)
.BYTE $24            ; add16 stk stk,
                     ;   pop
.BYTE $08, $83, $54  ; pop16 (imm16)
.BYTE $04, $83, $54  ; push16 (imm16)
.BYTE $02, $B8, $0B  ; push imm16
.BYTE $32            ; cmplt16 stk stk,
                     ;   pop
.BYTE $10, $0D, $00  ; btrue rel imm16
.BYTE $04, $83, $54  ; push16 (imm16)
.BYTE $02, $B6, $0B  ; push imm16
.BYTE $3E            ; sub stk, stk
.BYTE $08, $83, $54  ; pop16 (imm16)
.BYTE $B2, $E2, $52  ; next

                 --^--

It's literally a sum of the bytes in
the buffer, with a subtraction when the
value exceeds a threshold.

Using a smaller return value in the
initial protection routine (which we
did to fake success) causes this
checksum routine to behave a bit
differently.  The problem manifests
itself in this code:

                 --v--

.BYTE $F6, $2D       ; push16 imm8
.BYTE $F0            ; push0
.BYTE $0A, $B6, $52  ; pop16 (imm16+
                     ;   pop16*2)
.BYTE $04, $83, $54  ; push16 (imm16)
.BYTE $F6, $47       ; push16 imm8
.BYTE $2C            ; cmpeq16 stk stk,
                     ;   pop
.BYTE $10, $04, $00  ; bfalse rel imm16

                 --^--

The threshold isn't reached anymore on
one pass because the sum is too low, so
the resulting value is too large to
compare against a 8-bit value.

One solution is to adjust the threshold
so that the subtraction happens again.
If we lower the threshold, then the
subtraction happens again, but then a
different pass fails to trigger a
subtraction because of the distribution
of values in this buffer. If we raise
the threshold, then everything works
again.

Given a different set of values, the
opposite case could be true instead.

We raise the threshold and then we run
the game again.

The game plays for a looong time and
everything looks fine. That is, until
it prints "Fatal error: nnn" (the
number changes each time) and hangs.

Damn.

                   ~

               Chapter 3
 In Which We Are Getting Really Tired
     Of Having Our Fears Confirmed


Backtracking again, we find a new timer
setting. Track $1E sector $04, also
from the file named "B":

                 --v--

.BYTE $04, $AF, $51  ; push16 (imm16)
.BYTE $04, $68, $53  ; push16 (imm16)
.BYTE $24            ; add16 stk, stk
.BYTE $5C            ; push00xx (stk16)
.BYTE $F0            ; push0
.BYTE $2E            ; cmpne16 stk, stk
.BYTE $0E, $0C, $00  ; btrue rel imm16
.BYTE $F6, $48       ; push16 imm8
.BYTE $F6, $48       ; push16 imm8
.BYTE $5E            ; rand stk, stk
.BYTE $24            ; add16 stk, stk
.BYTE $08, $66, $53  ; pop16 (imm16)

                 --^--

This one is also checking if a
particular memory location is zero. If
that zero is found, then $5366 is
replaced with a RND(72)+72.

A quick bit of math to get the address,
and some backtracking to find it,
reveals the routine. Track $1F sector
$04, also from the file named "B":

                 --v--

.BYTE $12, $27, $00  ; jsr rel imm16
.BYTE $04, $AF, $61  ; push16 (imm16)
.BYTE $F6, $34       ; push16 imm8
.BYTE $24            ; add16 stk, stk
.BYTE $F6, $FF       ; push16 imm8
.BYTE $5C            ; push00xx (stk16)
.BYTE $C8            ; pop8 (stk16)

                 --^--

The result of the JSR is stored to a
calculated memory location. What does
the JSR do? It calls a native function.
What does the native function do? It
checks the disk again like the first
protection routine.

We've found the fourth protection
routine, and confirmed possibility #1
(and by extension, #3).

The code for the fourth protection
routine looks like this:

                 --v--

$E251 AD 14 E1   LDA $E114
$E254 20 98 E3   JSR $E398
$E257 AD 14 E1   LDA $E114
$E25A 20 AA E2   JSR $E2AA
$E25D 20 8D E2   JSR $E28D
$E260 90 0B      BCC $E26D
$E262 CE F6 E2   DEC $E2F6
$E265 30 0A      BMI $E271
$E267 20 35 E3   JSR $E335
$E26A 4C 51 E2   JMP $E251
$E26D A9 00      LDA #$00
$E26F F0 02      BEQ $E273
$E271 A9 FF      LDA #$FF
$E273 48         PHA
...

It checks in a loop for the special
track, then returns success or failure.

Seems like a simple change. There's
just one thing wrong, though: searching
the disk doesn't find that code.

The reason is that it's all byte-
swapped. And relocated.

Track $0B sector $0D, from the file
named "SHR1":

                 --v--

; #$61 becomes #$E2
$E26B 4C A9 61   JMP $61A9
$E26E F0 00      BEQ $E270
$E270 A9 02      LDA #$02
$E272 48         PHA
$E273 FF         ???

                 --^--

Eeew. Still, replacing that #$FF should
fix it.

More time passes... It is getting dark.
You are likely to be eaten by an anti-
tamper grue.

The game plays for a looong time and
everything looks fine. That is, until
it prints "Fatal error: nnn" (the
number changes each time) and hangs.

Sigh.

Backtracking again, we find that the
array has a different entry zeroed out.

We've found the fifth protection
routine.

                   ~

               Chapter 4
        In Which Words Are Hard
          But Sums Are Harder


But here's a new thing: the game plays
for a looong time and everything looks
fine. Really fine. It doesn't hang
anymore. So we're done. Celebrate!

Well, no. There's that pesky manual
protection that needs to go away.

We could cheat and make any answer
work. Yes, that's one way to do it, and
it's what I wanted to do to be done
with it. But 4am said "no," so no. If
you type nothing at the codeword lookup
screen, the game enters demonstration
mode. We want this mode to remain
available, so the "type anything"
option won't work.

We choose to take a different path. The
first thing is to reverse the logic so
that typing nothing would enter the
game proper, and typing the proper word
would enter demonstration mode.

I fix that.

Except that the "fatal error" message
came back. Yes, the p-code itself has a
checksum. We've found the sixth
protection routine.

Another point of interest regarding the
manual check is that if you type the
wrong word, the game prints "You are an
enemy spy" and then enters demo mode.

I found the check that causes the
"enemy spy" text to be printed. I
change it to print the "demonstration
mode" text instead.

The "fatal error" message came back.

We've found the seventh protection
routine. Yes, that part of the p-code
has a separate checksum.

I fix the checksum for that.

Great, but the message still asks for a
word from the manual. We want to fix
that.

I spent some hours crafting the perfect
wording for the prompt. It was crap and
we threw it out.

4am spent some minutes crafting some
wording for the prompt. It was perfect.
That's a fine skill.

I put the text in. Of course, the
"fatal error" message came back. Yes,
that part of the p-code also has a
*separate* checksum.

We've found the eighth protection
routine.

I fix that, and we're done, and it's
glorious.

The game is cracked for the first time
ever.

Quod erat liberandum.

                   ~

            Acknowledgments


Thanks to Ian Baronofsky for lending us
the original disk at Kansasfest 2017.

Thanks to 4am for editing and reviewing
drafts of this write-up.

---------------------------------------
docs by qkumba                 No. 1353
------------------EOF------------------
